<# .SYNOPSIS Configure Recovery Console security options using positional arguments - Secpol Policy Framework. .SCRIPTTYPE Computer Configuration .DESCRIPTION This script applies Recovery Console security option settings using positional arguments based on the PolicyDatabase array. Policies configured: 1. Recovery console: Allow automatic administrative logon - 0=Disabled, 1=Enabled 2. Recovery console: Allow floppy copy and access to all drives and folders - 0=Disabled, 1=Enabled NOTE: These settings are mainly relevant for older Windows versions that support Recovery Console. Automatic logon affects security vs. convenience trade-off for system repair scenarios. .PARAMETER PolicyValues JSON array string containing 2 policy values (in order). Use empty strings "" to skip policies. Format: '["value1","value2"]' .PARAMETER LogLevel Logging verbosity: Silent, Normal, Verbose, Debug .PARAMETER LogPath Custom log file path (optional) .PARAMETER WhatIf Preview changes without applying them .EXAMPLE .\Set-RecoveryConsoleSecurityOptions.ps1 '["0","0"]' Disables both recovery console automatic logon and floppy access .EXAMPLE .\Set-RecoveryConsoleSecurityOptions.ps1 '["0",""]' -WhatIf Preview disabling automatic logon only #> param( [Parameter(Position=0, ValueFromRemainingArguments=$true)] [string[]]$PolicyValuesArray = @("[]"), [ValidateSet('Silent','Normal','Verbose','Debug')] [string]$LogLevel = 'Normal', [string]$LogPath = $null, [switch]$WhatIf ) # Combine all arguments into a single PolicyValues string # First, try to get the original command line with proper quotes $PolicyValues = $null try { $currentPID = $PID Write-Host "Current Process ID: $currentPID" -ForegroundColor Cyan $process = Get-CimInstance Win32_Process -Filter "ProcessId = $currentPID" if ($process) { $commandLine = $process.CommandLine Write-Host "Full command line: $commandLine" -ForegroundColor Yellow if ($commandLine) { $scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path) $escapedScriptName = [regex]::Escape($scriptName) $pattern = "-File\s+`"[^`"]*\\$escapedScriptName`"\s+(.+?)(?:\s+(?:-LogLevel|-LogPath|-WhatIf)|$)" Write-Host "Using regex pattern: $pattern" -ForegroundColor DarkGray if ($commandLine -match $pattern) { $rawArgument = $matches[1].Trim() Write-Host "Raw argument extracted: $rawArgument" -ForegroundColor Magenta if ($rawArgument -match '^"(.*)"$') { $PolicyValues = $matches[1] } else { $PolicyValues = $rawArgument } Write-Host "Extracted PolicyValues from command line: $PolicyValues" -ForegroundColor Green } else { Write-Host "Command line regex did not match. Command line: $commandLine" -ForegroundColor Red } } else { Write-Host "CommandLine property is null or empty" -ForegroundColor Red } } else { Write-Host "Failed to get process information for PID $currentPID" -ForegroundColor Red } } catch { Write-Host "Error extracting from command line: $($_.Exception.Message)" -ForegroundColor Red Write-Verbose "Could not extract from command line: $($_.Exception.Message)" } if (-not $PolicyValues) { $PolicyValues = if ($PolicyValuesArray.Count -gt 1) { $PolicyValuesArray -join '' } else { $PolicyValuesArray[0] } Write-Verbose "Using parameter-based PolicyValues: $PolicyValues" } # Input DB - Recovery Console Security Options # RegistryType: 4=DWORD, 1=String, 7=MultiString, 3=Binary $PolicyDatabase = @( @{ Name = "Recovery console: Allow automatic administrative logon" KeyGroup = "[Registry Values]" Key = "MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Setup\RecoveryConsole\SecurityLevel" RegistryType = 4 }, @{ Name = "Recovery console: Allow floppy copy and access to all drives and folders" KeyGroup = "[Registry Values]" Key = "MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Setup\RecoveryConsole\SetCommand" RegistryType = 4 } ) # Script-wide variables $script:LogFile = $null $script:StartTime = Get-Date $script:ProcessedCount = 0 $script:SuccessCount = 0 $script:FailureCount = 0 $script:SkippedCount = 0 # Initialize logging function Initialize-LogPath { if ($LogPath) { $logDir = Split-Path $LogPath -Parent if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return $LogPath } # Try to get agent directory, fallback to script directory $baseDir = $PSScriptRoot try { $registryPath = if ([Environment]::Is64BitOperatingSystem) { "HKLM:\SOFTWARE\WOW6432Node\AdventNet\DesktopCentral\DCAgent" } else { "HKLM:\SOFTWARE\AdventNet\DesktopCentral\DCAgent" } if (Test-Path $registryPath) { $agentDir = (Get-ItemProperty -Path $registryPath -Name "DCAgentInstallDir" -ErrorAction SilentlyContinue).DCAgentInstallDir if ($agentDir -and (Test-Path $agentDir)) { $baseDir = $agentDir } } } catch { # Silently fall back to script directory } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $logDir = Join-Path (Join-Path $baseDir "logs") "SecurityPolicies" if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return Join-Path $logDir "RecoveryConsoleSecurityOptions_$timestamp.log" } # Logging function with log levels function Write-Log { param( [string]$Message, [ValidateSet('INFO','SUCCESS','WARNING','ERROR','DEBUG','PROGRESS')] [string]$Level = 'INFO', [string]$Component = 'Main' ) $levelPriority = @{ 'Silent' = 0 'Normal' = 1 'Verbose' = 2 'Debug' = 3 } $messagePriority = @{ 'ERROR' = 0 'WARNING' = 0 'SUCCESS' = 1 'PROGRESS' = 1 'INFO' = 2 'Debug' = 3 } $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logEntry = "[$timestamp] [$Level] [$Component] $Message" # Always write to log file if ($script:LogFile) { Add-Content -Path $script:LogFile -Value $logEntry -ErrorAction SilentlyContinue } # Console output based on log level if ($levelPriority[$LogLevel] -ge $messagePriority[$Level]) { switch ($Level) { 'ERROR' { Write-Host $logEntry -ForegroundColor Red } 'WARNING' { Write-Host $logEntry -ForegroundColor Yellow } 'SUCCESS' { Write-Host $logEntry -ForegroundColor Green } 'DEBUG' { Write-Host $logEntry -ForegroundColor Cyan } 'PROGRESS'{ Write-Host $logEntry -ForegroundColor Magenta } default { Write-Host $logEntry } } } } # Progress logging function function Write-ProgressLog { param([string]$Message, [string]$Component = 'Progress') Write-Log -Message $Message -Level 'PROGRESS' -Component $Component } # Check if running as administrator function Test-Admin { $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Initialize script function Initialize-Script { $script:LogFile = Initialize-LogPath Write-Log "========================================" -Level 'INFO' Write-Log "Recovery Console Security Options Configuration Started" -Level 'INFO' Write-Log "Script: $($MyInvocation.ScriptName)" -Level 'INFO' Write-Log "Log Level: $LogLevel" -Level 'INFO' Write-Log "WhatIf Mode: $WhatIf" -Level 'INFO' Write-Log "========================================" -Level 'INFO' if (-not (Test-Admin)) { Write-Log "ERROR: This script requires administrator privileges" -Level 'ERROR' exit 1 } } # Import INF file into structured data function Import-InfFile { param([string]$Path) if (-not (Test-Path $Path)) { Write-Log "INF file not found: $Path" -Level 'ERROR' return $null } $infData = @{} $currentSection = $null Get-Content $Path -Encoding Unicode | ForEach-Object { $line = $_.Trim() # Skip empty lines and comments if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(';')) { return } # Section header if ($line -match '^\[(.+)\]$') { $currentSection = $matches[1] if (-not $infData.ContainsKey($currentSection)) { $infData[$currentSection] = @{} } return } # Key-value pair if ($currentSection -and $line -match '^(.+?)\s*=\s*(.*)$') { $key = $matches[1].Trim() $value = $matches[2].Trim() $infData[$currentSection][$key] = $value } } return $infData } # Write INF data back to file function Write-InfFile { param( [hashtable]$Data, [string]$Path ) $output = @() $output += "[Unicode]" $output += "Unicode=yes" # Ensure [Version] section is first if ($Data.ContainsKey('Version')) { $output += "[Version]" foreach ($key in $Data['Version'].Keys) { $output += "$key=$($Data['Version'][$key])" } } # Write other sections foreach ($section in ($Data.Keys | Where-Object { $_ -notin @('Unicode','Version') } | Sort-Object)) { $output += "[$section]" foreach ($key in ($Data[$section].Keys | Sort-Object)) { $output += "$key=$($Data[$section][$key])" } } $output | Out-File -FilePath $Path -Encoding unicode -Force } # Set individual secpol row function Set-SecpolRow { param( [string]$Name, [string]$KeyGroup, [string]$Key, [string]$Value, [int]$RegistryType, [hashtable]$PolicyData ) if ([string]::IsNullOrWhiteSpace($Value)) { Write-Log "Skipping ''$Name'' - No value provided" -Level 'WARNING' -Component $Name $script:SkippedCount++ return $false } # Special handling for string values (RegistryType = 1 or 7): Convert newlines to commas for INF format compatibility if ($RegistryType -eq 1 -or $RegistryType -eq 7) { if ($Value -match '(\r\n|\n|\r)') { # Replace various newline formats with commas $Value = $Value -replace '(\r\n|\n|\r)', ',' # Remove any trailing commas $Value = $Value.TrimEnd(',') Write-Log "Converted multi-line text to comma-separated format for INF compatibility" -Level 'DEBUG' -Component $Name } } Write-ProgressLog "Processing: $Name" -Component $Name Write-Log "Setting: $Name = $Value" -Level 'INFO' -Component $Name Write-Log "Location: $KeyGroup\$Key" -Level 'DEBUG' -Component $Name if ($WhatIf) { Write-Log "[WhatIf] Would set $KeyGroup\$Key = $Value" -Level 'INFO' -Component $Name $script:SuccessCount++ return $true } try { # Update policy data with proper registry type format $sectionName = $KeyGroup.Trim('[',']') if (-not $PolicyData.ContainsKey($sectionName)) { $PolicyData[$sectionName] = @{} } # Format: key=RegistryType,Value (e.g., key=4,1 for DWORD with value 1) $PolicyData[$sectionName][$Key] = "$RegistryType,$Value" Write-Log "Successfully configured: $Name" -Level 'SUCCESS' -Component $Name $script:SuccessCount++ return $true } catch { Write-Log "Failed to configure ''$Name'': $($_.Exception.Message)" -Level 'ERROR' -Component $Name $script:FailureCount++ return $false } } # Save all changes to security policy # Parse PolicyValues array string to array function Parse-PolicyValuesArray { param([string]$ArrayString) # Check if it looks like JSON format if ($ArrayString -match '^\s*\[.*\]\s*$') { try { # Fix common JSON format issues: unquoted alphanumeric values $FixedArrayString = $ArrayString -replace '\[(\w+)(,|])', '["$1"$2' -replace ',(\w+)(,|])', ',"$1"$2' # Parse the JSON-like array string $Arguments = ConvertFrom-Json $FixedArrayString Write-Log "Successfully parsed policy values array: $($Arguments.Count) values provided" -Level Info return $Arguments } catch { Write-Log "Failed to parse PolicyValues array string: $($_.Exception.Message)" -Level Error Write-Log "Expected format: '[\"value1\",\"value2\",\"value3\"]'" -Level Warning return @() } } else { # Not JSON format - use PolicyValuesArray directly as positional arguments Write-Log "Using positional arguments format (non-JSON): $($PolicyValuesArray.Count) values provided" -Level Info return $PolicyValuesArray } } # Parse PolicyValues array string to get individual arguments $Arguments = Parse-PolicyValuesArray -ArrayString $PolicyValues function Save-SecpolChanges { param([hashtable]$PolicyData) $exportPath = Join-Path $PSScriptRoot "secpol_export.inf" $modifiedPath = Join-Path $PSScriptRoot "secpol_modified.inf" try { Write-ProgressLog "Saving changes to security policy database..." # Write modified policy Write-InfFile -Data $PolicyData -Path $modifiedPath Write-Log "Modified policy written to: $modifiedPath" -Level 'DEBUG' # Apply the policy Write-Log "Applying security policy configuration..." -Level 'INFO' $seceditOutput = secedit /configure /db secedit.sdb /cfg $modifiedPath /areas SECURITYPOLICY 2>&1 if ($LASTEXITCODE -eq 0) { Write-Log "Security policy applied successfully" -Level 'SUCCESS' return $true } else { Write-Log "Secedit returned exit code: $LASTEXITCODE" -Level 'ERROR' Write-Log "Secedit output: $seceditOutput" -Level 'ERROR' return $false } } catch { Write-Log "Error saving security policy: $($_.Exception.Message)" -Level 'ERROR' return $false } } # Main execution Initialize-Script try { if ($PolicyDatabase.Count -eq 0) { Write-Log "Policy database is empty. Please populate the PolicyDatabase array." -Level 'WARNING' exit 0 } # Export current policy $exportPath = Join-Path $PSScriptRoot "secpol_export.inf" Write-Log "Exporting current security policy..." -Level 'INFO' secedit /export /cfg $exportPath /areas SECURITYPOLICY | Out-Null # Import current policy data $policyData = Import-InfFile -Path $exportPath if (-not $policyData) { Write-Log "Failed to import current security policy" -Level 'ERROR' exit 1 } # Process each policy with its corresponding argument for ($i = 0; $i -lt $PolicyDatabase.Count; $i++) { $policy = $PolicyDatabase[$i] $value = if ($i -lt $Arguments.Count) { $Arguments[$i] } else { $null } $script:ProcessedCount++ Set-SecpolRow -Name $policy.Name -KeyGroup $policy.KeyGroup -Key $policy.Key -Value $value -RegistryType $policy.RegistryType -PolicyData $policyData } # Save all changes if not in WhatIf mode and there were successful changes if (-not $WhatIf -and $script:SuccessCount -gt 0) { Save-SecpolChanges -PolicyData $policyData } } catch { Write-Log "Critical error in main execution: $($_.Exception.Message)" -Level 'ERROR' Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level 'DEBUG' } finally { # Summary $duration = (Get-Date) - $script:StartTime Write-Log "========================================" -Level 'INFO' Write-Log "Execution Summary:" -Level 'INFO' Write-Log " Total Processed: $($script:ProcessedCount)" -Level 'INFO' Write-Log " Successful: $($script:SuccessCount)" -Level 'INFO' Write-Log " Failed: $($script:FailureCount)" -Level 'INFO' Write-Log " Skipped: $($script:SkippedCount)" -Level 'INFO' Write-Log " Duration: $($duration.TotalSeconds) seconds" -Level 'INFO' Write-Log "========================================" -Level 'INFO' }